package br.unifor.cct.mia.mutation;

import java.awt.Point;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;

import weka.core.Instance;
import weka.core.Instances;
import weka.core.Utils;

import br.unifor.cct.mia.coevolution.InvalidTypeException;
import br.unifor.cct.mia.dataenhancement.Attribute;
import br.unifor.cct.mia.dataenhancement.Database;
import br.unifor.cct.mia.dataenhancement.GenotypeConverter;
import br.unifor.cct.mia.dataenhancement.Structure;
import br.unifor.cct.mia.evolutionary.Genotype;
import br.unifor.cct.mia.evolutionary.SpeciesConstants;
import br.unifor.cct.mia.ga.Ga;
import br.unifor.cct.mia.util.Methods;

public class MutationKNN implements Mutation {

	private Instances m_Train;
	private double [] m_MinArray;
	private double [] m_MaxArray;
	private Instances instances;
	private List positions;
	private Structure st;
	private Database db;
	private int kValue=3;

	private void mutate(Ga ga) {        	
		double lbound, ubound;
		double x;

		for (int i = 0; i < ga.configurations.getPopsize(); i++) {

			int knnIndex[] = new int[kValue];
			double knnValues[] = new double[kValue];
			Arrays.fill(knnIndex, Integer.MAX_VALUE);
			Arrays.fill(knnValues, Integer.MAX_VALUE);

			Instance first = instances.instance(i);
			for (int y=0; y<ga.configurations.getPopsize(); y++) {
				if ( y==i ) continue;
				Instance second = instances.instance(y);
				double dist = distance(first, second);

				double maxValue[] = Methods.arrayMaximunValuePos(knnValues);									
				double minDist = maxValue[0];
				int minIndex = (int)maxValue[1];

				if ( dist<minDist ) {
					knnValues[minIndex] = dist;
					knnIndex[minIndex] = y;
				}
			}


			for (int j = 0; j < ga.configurations.getNvars(); j++) {

				x = Methods.randDoubleValue(0, 1);
				if (x*100 < 50) {

					x = Methods.randDoubleValue(0, 1);
					if (x*100 < ga.configurations.getPmutation()*100) {
						lbound = ((Genotype)ga.population[i]).getLower(j);
						ubound = ((Genotype)ga.population[i]).getUpper(j);

						if (((Genotype)ga.population[i]).getInteger(j))
							((Genotype)ga.population[i]).setGene(Methods.randIntValue((int)lbound, (int)ubound), j);
						else
							((Genotype)ga.population[i]).setGene(Methods.randDoubleValue(lbound,ubound), j);
					}

				}
				else {

					x = Methods.randDoubleValue(0, 1);
					if (x*100 < 30) {

						Point p = (Point)positions.get(j);
						Attribute att = st.getAttribute(p.y);

						if (att.getTipo().equalsIgnoreCase(Attribute.CONTINUOUS)) {	

							double sun = 0;
							for( int minIndex:knnIndex ) {
								Instance vizinho = instances.instance(minIndex);
								if ( vizinho.isMissing(p.y) ) continue;
								sun += vizinho.value(p.y);
							}

							double valor = (double)((double)sun/(double)kValue);							
							((Genotype)ga.population[i]).setGene(valor,j);

						}
						else {	

							int[] count = new int[kValue];
							List<String> values = new ArrayList<String>();

							for( int minIndex:knnIndex ) {
								Instance nn = instances.instance(minIndex);									
								String v = nn.stringValue(p.y);									
								if (!values.contains(v)) values.add(v);

								int index = values.indexOf(v);
								count[index]++;
							}

							int pos = Methods.arrayMaximunPos(count);
							String valor = values.get(pos);

							int posValor = st.getValues(att).indexOf(valor);
							((Genotype)ga.population[i]).setGene(posValor,j);
						}
					}					

				}
			}
		}
	}

	public Object execute(Object value) {
		Object[] o = (Object[])value;
		Ga ga = (Ga)o[0];
		positions = (List)o[1];
		kValue = (Integer)o[2];


		st = ga.getStructure();
		db = ga.getDbOriginal();

		File file = null;
		try {
			file = new GenotypeConverter().convert(null,
					SpeciesConstants.FULL_DATABASE,
					"temp/tempKNNOperator.txt",
					st,db,null,null);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (InvalidTypeException e) {
			e.printStackTrace();
		}


		try {
			instances = new Instances(new BufferedReader(new FileReader(file)));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		m_Train = new Instances(instances, 0, instances.numInstances());
		m_Train.setClassIndex(m_Train.numAttributes()-1);
		m_Train.deleteWithMissingClass();

		m_MinArray = new double [m_Train.numAttributes()];
		m_MaxArray = new double [m_Train.numAttributes()];
		for (int i = 0; i < m_Train.numAttributes(); i++) {
			m_MinArray[i] = m_MaxArray[i] = Double.NaN;
		}
		Enumeration enu = m_Train.enumerateInstances();
		while (enu.hasMoreElements()) {
			updateMinMax((Instance) enu.nextElement());
		}


		mutate(ga);        
		return ga.population;
	}


	public double distance(Instance first, Instance second) {
		updateMinMax(first);
		double diff, distance = 0;

		for(int i = 0; i < m_Train.numAttributes(); i++) { 

			if (i == m_Train.classIndex()) {
				continue;
			}
			if (m_Train.attribute(i).isNominal()) {

				// If attribute is nominal
				if (first.isMissing(i) || second.isMissing(i) ||
						((int)first.value(i) != (int)second.value(i))) {
					distance += 1;
				}
			} else {

				// If attribute is numeric
				if (first.isMissing(i) || second.isMissing(i)){
					if (first.isMissing(i) && second.isMissing(i)) {
						diff = 1;
					} else {
						if (second.isMissing(i)) {
							diff = norm(first.value(i), i);
						} else {
							diff = norm(second.value(i), i);
						}
						if (diff < 0.5) {
							diff = 1.0 - diff;
						}
					}
				} else {
					diff = norm(first.value(i), i) - norm(second.value(i), i);
				}
				distance += diff * diff;
			}
		}

		return distance;
	}

	private double norm(double x,int i) {

		if (Double.isNaN(m_MinArray[i])
				|| Utils.eq(m_MaxArray[i], m_MinArray[i])) {
			return 0;
		} else {
			return (x - m_MinArray[i]) / (m_MaxArray[i] - m_MinArray[i]);
		}
	}

	private void updateMinMax(Instance instance) {
		for (int j = 0;j < m_Train.numAttributes(); j++) {
			if ((m_Train.attribute(j).isNumeric()) && (!instance.isMissing(j))) {
				if (Double.isNaN(m_MinArray[j])) {
					m_MinArray[j] = instance.value(j);
					m_MaxArray[j] = instance.value(j);
				} else {
					if (instance.value(j) < m_MinArray[j]) {
						m_MinArray[j] = instance.value(j);
					} else {
						if (instance.value(j) > m_MaxArray[j]) {
							m_MaxArray[j] = instance.value(j);
						}
					}
				}
			}
		}
	}

}
